function [BVInfo, eegData] = loadbrainvisiondata( vhdrPath, encoding)
%LOADBRAINVISIONDATA loads EEG data in Brain Vision Data Exchange Format
%   LOADBRAINVISIONDATA load the Brain Vision data specified in the Brain 
%   Vision Header file vhdrPath. For the technique details of the "BrainVision 
%   Data Exchange Format", please refer the manual accompanied with the Brain 
%   Vision Recorder software. By invoke this function, you actually load the
%   infomation contained in the YourData.vhdr file, the raw EEG data in the
%   YourData.eeg file and if any the event markers in the YourData.vmkr
%   file into a MATLAB structure BVData.
%   
%   INPUT ARGUMENTS
%   vhdrPath: a char string which specifies the path of the vhdr file. 
%       vhdrPath can be either the full path of the header file or the path 
%       relative to the invoker of this function. For clarity, the
%       fullpath approach is the recommended one.
%   encoding: a char string which specifies the encoding of the vhdr file. If
%       ommited, the default encoding scheme would be used, that is 'UTF-8'. For
%       a complete list of encoding scheme, please refer the correspongding
%       MATLAB reference page of fopen.
%
%   OUTPUT ARGUMENTS
%   BVInfo: MATLAB structure which contains the eeg data information.
%       BVInfo.vhdr corresponds to  the vhdr file and BVInfo.vmrk to the
%       vmkr file. BVInfo.eeg is the memory mapping object, which contains
%       the mapping infomation of the eeg data.
%   eegData: MATLAB array which contains the eegdata. Dimension of this
%       array is nChannel*nPoint, in which nChannel is the number of the eeg
%       channels.
%   
%   BVInfo = loadBrainVisionData( vhdrPath, [encoding] ) 
%   
%   ALTNATIVE: maybe you can reimplement this funcion using the MATLAB
%   genvarname function, however I use the predefined approach to overcome
%   the inconsistency of the section names and field names of the vhdr
%   files and vmrk files.
%
%   NOTE: loadbrainvisiondata is intended for internal use of the rtEEG
%   platform, so it assumes that the header file with the ext 'vhdr' and
%   the marker file with the ext 'vmrk' both conform the ini file
%   specification by the Brain Vision Recorder Manual.

%   Copyright 2012-2015 Kang M.X. Beijing Normal University.
%   $Revision: 1.0.0 $ $Date: 2014/2/26 22:19:00 $
%   First edition of the function loadbrainvisiondata
%   $Revision: 2.0.0 $ $Date: 2014/3/20 22:19:00 $
%   Reimplement this function using memory mapping file to boost the
%   performance.

% check the validity of the input
if exist(vhdrPath, 'file') ~= 2
    error('UTILITIES:LDBVDATA:FileNonexistException', ...
        [vhdrPath, ' doesn''t exist or is an invalid path.']);
end

if nargin < 2
    encoding = 'UTF-8';
end



% create the singleton of the BVData struct
% For fields with a default value, the default value sepecified in the
% Recorder's reference manual is used. For fields without a default value,
% an empty value of the specified type(e.g. char array, or numeric) is
% used. For more details please reference the manual accompanied with 
% your Brain Vision Recorder software.
BVInfo.vhdr = struct(...
    'description', '',...
    'CommonInfos', [], ...
    'ASCIIInfos', [], ...
    'BinaryInfos', [], ...
    'ChannelInfos', [], ...
    'Comment', '');
BVInfo.vhdr.CommonInfos = struct(...
    'DataFile', '', ...                         % Essential field
    'MarkerFile', '', ...
    'DataFormat', 'ASCII', ...
    'DataOrientation', 'MULTIPLEXED', ...
    'DataType', 'TIMEDOMAIN', ...
    'NumberOfChannels', [], ...                 % Essential field
    'SamplingInterval', [], ...                 % Eseential field
    'Averaged', 'NO', ...
    'AveragedSegments', 0, ...
    'SegmentDataPoints', 0, ...
    'SegmentationType', 'NOTSEGMENTED', ...
    'DataPoints', 0, ...
    'Codepage', 'ANSI');
BVInfo.vhdr.ASCIIInfos = struct(...
    'DecimalSymbol', '.', ...
    'SkipLines', [], ...
    'SkipColumns', []);
BVInfo.vhdr.BinaryInfos = struct(...
    'BinaryFormat', 'INT_16', ...
    'ChannelOffset', 0, ...
    'DataOffset', 0, ...
    'SegmentHeaderSize', 0, ...
    'TrailerSize', 0, ...
    'UseBigEndianOrder', 'NO');
    
BVInfo.vmrk = struct(...
    'description', '',...
    'CommonInfos', struct('DataFile', ''), ...
    'MarkerInfos', []);

BVInfo.eeg = [];



% load the information in the header file i.e. the file with the ext 'vhdr'
fidHeader = fopen(vhdrPath, 'rt', 'n', encoding);
if fidHeader == -1
    error('UTILITIES:LDBVDATA:FileOpenErrorException', ...
        ['can not open ''', vhdrPath, '''']);
end

descriptionStatus = 'READY';   % indication of whether we have capture the vhdr 
                               % desription, i.e. READY MISSED                          
while ~feof(fidHeader)
    currentLine = strtrim(fgetl(fidHeader)); 
    if isempty(currentLine) || currentLine(1) == ';'    % skip empty lines and comments
        continue;
    end
    
    if currentLine(1) == '[' && currentLine(end) == ']' % ini section entry
        if strcmp(descriptionStatus, 'READY')
            descriptionStatus = 'MISSED';   % there isn't a description text
        end
        
        sectionName = genvarname(currentLine(2:end-1));
        if isfield(BVInfo.vhdr, sectionName)
            continue;
        else
            warndlg(['Add field ', sectionName, 'to BVData.vhdr.'], ...
                'WARNING');
            BVInfo.vhdr.(sectionName) = [];
        end
    elseif strcmp(descriptionStatus, 'READY')           % vhdr description text
        BVInfo.vhdr.description = [BVInfo.vhdr.description, currentLine];
    else                                                % ini field entry
        equalSignPosition = strfind(currentLine, '=');
        fieldName = strtrim(currentLine(1:equalSignPosition-1));
        fieldContent = strtrim(currentLine(equalSignPosition+1:end));
        switch sectionName
            case 'ChannelInfos'
                BVInfo.vhdr.(sectionName).(fieldName) ...
                = parsechannelinfo(fieldContent);
            case 'Comment'
                continue;       % just skip the comments
            otherwise
                if isa(BVInfo.vhdr.(sectionName).(fieldName), 'double')
                    BVInfo.vhdr.(sectionName).(fieldName) = str2double(fieldContent);
                else
                    BVInfo.vhdr.(sectionName).(fieldName) = fieldContent;
                end
        end
    end
end

fclose(fidHeader);

% Check if the essential fields have been filled or not
if isempty(BVInfo.vhdr.CommonInfos.NumberOfChannels) || ...
    isempty(BVInfo.vhdr.CommonInfos.SamplingInterval)
    error('UTILITIES:LDBVDATA:EssentialFielsNonexistException', ...
        'Essential fields of the vhdr file are either empty or nonexist.');  
end
% Adjust the vhdr file fields according to the ini file specification in
% the Recorder's Manual.
[~, name, ext] = fileparts(vhdrPath);
if isempty(BVInfo.vhdr.CommonInfos.DataFile)
    BVInfo.vhdr.CommonInfos.DataFile = [name, ext];
elseif strfind(BVInfo.vhdr.CommonInfos.DataFile, '$b')
    BVInfo.vhdr.CommonInfos.DataFile = strrep(BVInfo.vhdr.CommonInfos.DataFile, '$b', name);
end



% Load the information in the marker file
if ~isempty(BVInfo.vhdr.CommonInfos.MarkerFile)
    if strfind(BVInfo.vhdr.CommonInfos.MarkerFile, '$b')
        BVInfo.vhdr.CommonInfos.MarkerFile = strrep(BVInfo.vhdr.CommonInfos.MarkerFile, '$b', name);
    end
    strMarkerFilePath = fullfile(fileparts(vhdrPath), ...
        BVInfo.vhdr.CommonInfos.MarkerFile);
    fidMarker = fopen(strMarkerFilePath, 'rt', 'n', encoding);
    if fidMarker == -1 
        error('UTILITIES:LDBVDATA:FileOpenErrorException', ...
            ['can not open ''', strMarkerFilePath, '''']);
    end    
    
    descriptionStatus = 'READY';
    while ~feof(fidMarker)
        currentLine = strtrim(fgetl(fidMarker)); 
        if isempty(currentLine) || currentLine(1) == ';'    % skip empty lines and comments
            continue;
        end
        
        if currentLine(1) == '[' && currentLine(end) == ']' % ini section entry
            if strcmp(descriptionStatus, 'READY')
                descriptionStatus = 'MISSED';   % there isn't a description text
            end

            sectionName = genvarname(currentLine(2:end-1));
            if isfield(BVInfo.vmrk, sectionName)
                continue;
            else
                warndlg(['Add field ', sectionName, 'to BVData.vmrk.'], ...
                    'WARNING');
                BVInfo.vmrk.(sectionName) = [];
            end
        elseif strcmp(descriptionStatus, 'READY')           % vmrk description text
            BVInfo.vmrk.description = [BVInfo.vmrk.description, currentLine];
        else                                                % ini field entry
            equalSignPosition = strfind(currentLine, '=');  % get the field name and its value
            fieldName = strtrim(currentLine(1:equalSignPosition-1));
            fieldContent = strtrim(currentLine(equalSignPosition+1:end));
            switch sectionName
                case 'CommonInfos'
                    if isa(fieldContent, 'double')
                        BVInfo.vmrk.(sectionName).(fieldName) = str2double(fieldContent);
                    else
                        BVInfo.vmrk.(sectionName).(fieldName) = fieldContent;
                    end              
                case 'MarkerInfos'
                    BVInfo.vmrk.(sectionName).(fieldName) ...
                    = parsemarkerinfo(fieldContent);
                otherwise
                    % Do if there are more sections in the marker file
                    
            end     % switch
        end        
    end
    
    fclose(fidMarker);
end



% load the eeg data
strDataFilePath = fullfile(fileparts(vhdrPath), BVInfo.vhdr.CommonInfos.DataFile);
fidEEG = fopen(strDataFilePath, 'rb', 'n');
if fidEEG == -1
    error('UTILITIES:LDBVDATA:FileOpenErrorException', ...
        ['can not open ''', strDataFilePath]);
end

nChannels = BVInfo.vhdr.CommonInfos.NumberOfChannels;

switch BVInfo.vhdr.CommonInfos.DataFormat
    case 'BINARY'
        strBinaryFormat = BVFormat2MATLABFormat(BVInfo.vhdr.BinaryInfos.BinaryFormat);
%         sctDataFileProperty = dir(strDataFilePath);
%         nDataFileBytes = sctDataFileProperty.bytes;
%         nDataNumber = nDataFileBytes ./ (nChannels * nByte);
%         BVData.eeg = zeros(nChannels, nDataNumber + 10, strBinaryFormat);   % preallocate space for eeg data.
%         BVData.eeg = fread(fidEEG, [nChannels, inf], strBinaryFormat);

% Be careful to modify the following codes, or you will solw down this
% function!!
        BVInfo.eeg = memmapfile(strDataFilePath, 'Format', strBinaryFormat);
        eegData = reshape(BVInfo.eeg.data, nChannels, []);
    case 'ASCII'
        % preserved for ASCII data 
end

fclose(fidEEG);
end



% BVFORMAT2MATLABFORMAT converts Brain Vision binary format into MATLAB
% format
function [strMATLABFormat, nByte] = BVFormat2MATLABFormat(strBVFormat)
    switch strBVFormat
        case 'IEEE_FLOAT_32'
            strMATLABFormat = 'single';
            nByte = 4;
        case 'INT_16'
            strMATLABFormat = 'int16';
            nByte = 2;
        case 'UINT_16'
            strMATLABFormat = 'uint16';
            nByte = 2;
    end
end


%PARSECHANNELINFO parse the values of the channel settings, then return
% a cell array
function fieldElementCellArray = parsechannelinfo(currentField)
    currentField = strtrim(currentField);
    
    fieldElementCellArray = cell(1, 4);     % preallocate the cell array
    index = 1;
    while ~isempty(currentField)
        [fieldElementCellArray{index}, currentField] = getnextelement(currentField);
        index = index + 1;
    end
    
    fieldElementCellArray{3} = str2double(fieldElementCellArray{3});
    
% if there are more peroperties in the channel infos

end



%PARSEMARKERINFO parse the marker values, then return as a cell array
function fieldElementCellArray = parsemarkerinfo(currentField)
    currentField = strtrim(currentField);
    
    fieldElementCellArray = cell(1, 6);
    index = 1;
    while ~isempty(currentField)
        [fieldElementCellArray{index}, currentField] = getnextelement(currentField);
        index = index + 1;
    end
    
    fieldElementCellArray{3} = str2double(fieldElementCellArray{3});
    fieldElementCellArray{4} = str2double(fieldElementCellArray{4});
    fieldElementCellArray{5} = str2double(fieldElementCellArray{5});
end



%GETNEXTELEMENT getthe next element of the comma separated line
%   Unlike strtok GETNEXTELEMENT won't skip the tokens of zero length
function [nextElement, remain] = getnextelement(elementLine)
    commaPostition = strfind(elementLine, ',');
    if ~isempty(commaPostition)
        nextElement = elementLine(1 : commaPostition(1)-1);
        remain = elementLine(commaPostition(1)+1 : end);
    else
        nextElement = strtrim(elementLine);
        remain = [];
    end
end
